<?php
/**
 * @copyright 2009-2019 Vanilla Forums Inc.
 * @license GPL-2.0-only
 */

use Garden\Schema\Schema;
use Vanilla\Formatting\Formats\WysiwygFormat;
use Vanilla\Formatting\Html\HtmlDocument;
use Vanilla\Formatting\Html\Processor\ImageHtmlProcessor;
use Vanilla\Web\Asset\LegacyAssetModel;

/**
 * Class EmailTemplate
 *
 * Compiles the data for an email, applies appropriate content filters and renders the email.
 *
 * @author Becky Van Bussel <becky@vanillaforums.com>
 * @package Core
 * @since 2.2
 */
class EmailTemplate extends Gdn_Pluggable implements Gdn_IEmailTemplate
{
    /** Delimiter for plaintext email. */
    const PLAINTEXT_START = "<!-- //TEXT VERSION FOLLOWS//";

    /** Default email colors. */
    const DEFAULT_TEXT_COLOR = "#333333";
    const DEFAULT_BACKGROUND_COLOR = "#eeeeee";
    const DEFAULT_CONTAINER_BACKGROUND_COLOR = "#ffffff";
    const DEFAULT_BUTTON_BACKGROUND_COLOR = "#38abe3"; // Vanilla blue
    const DEFAULT_BUTTON_TEXT_COLOR = "#ffffff";

    // Component properties

    /** @var string The HTML formatted email title. */
    protected $title;

    /**  @var string The HTML formatted email lead (sub-title, appears under title). */
    protected $lead;

    /** @var string The HTML formatted email message (the body of the email). */
    protected $message;

    /**
     * @var array An array representing a footer with the following keys:
     *  'text' => The HTML-formatted footer text.
     *  'textColor' => The hex color code of the footer text, must include the leading '#'.
     *  'backgroundColor' => The hex color code of the footer background, must include the leading '#'.
     */
    protected $footer;

    /**
     * @var array An array representing a button with the following keys:
     *  'url' => The href value of the button.
     *  'text' => The button text.
     *  'textColor' => The hex color code of the button text, must include the leading '#'.
     *  'backgroundColor' => The hex color code of the button background, must include the leading '#'.
     */
    protected $button;

    /**
     * @var array An array representing an image with the following keys:
     *  'source' => The image source url.
     *  'link' => The href value of the image wrapper.
     *  'alt' => The alt value of the image tag.
     */
    protected $image;

    /** @var string The path to the email view. */
    protected $view;

    /** @var bool Whether to render in plaintext. */
    protected $plaintext = false;

    // Color properties

    /** @var string The hex color code of the text, must include the leading '#'.*/
    protected $textColor = self::DEFAULT_TEXT_COLOR;

    /** @var string The hex color code of the background, must include the leading '#'. */
    protected $backgroundColor = self::DEFAULT_BACKGROUND_COLOR;

    /** @var string The hex color code of the container background, must include the leading '#'. */
    protected $containerBackgroundColor = self::DEFAULT_CONTAINER_BACKGROUND_COLOR;

    /**@var string The default hex color code of the button text, must include the leading '#'. */
    protected $defaultButtonTextColor = self::DEFAULT_BUTTON_TEXT_COLOR;

    /** @var string The default hex color code of the button background, must include the leading '#'. */
    protected $defaultButtonBackgroundColor = self::DEFAULT_BUTTON_BACKGROUND_COLOR;

    /**
     * Template initial setup.
     *
     * @param string $message HTML formatted email message (the body of the email).
     * @param string $title HTML formatted email title.
     * @param string $lead HTML formatted email lead (sub-title, appears under title).
     * @param string $view
     *
     * @throws Exception
     */
    public function __construct($message = "", $title = "", $lead = "", $view = "email-basic")
    {
        parent::__construct();

        $this->setMessage($message);
        $this->setTitle($title);
        $this->setLead($lead);

        // Set templating defaults
        $this->setTextColor(c("Garden.EmailTemplate.TextColor", self::DEFAULT_TEXT_COLOR));
        $this->setBackgroundColor(c("Garden.EmailTemplate.BackgroundColor", self::DEFAULT_BACKGROUND_COLOR));
        $this->setContainerBackgroundColor(
            c("Garden.EmailTemplate.ContainerBackgroundColor", self::DEFAULT_CONTAINER_BACKGROUND_COLOR)
        );
        $this->setDefaultButtonBackgroundColor(
            c("Garden.EmailTemplate.ButtonBackgroundColor", self::DEFAULT_BUTTON_BACKGROUND_COLOR)
        );
        $this->setDefaultButtonTextColor(c("Garden.EmailTemplate.ButtonTextColor", self::DEFAULT_BUTTON_TEXT_COLOR));

        $this->setDefaultEmailImage();

        // Set default view
        $this->view = LegacyAssetModel::viewLocation($view, "email", "dashboard");
    }

    /**
     * Sets the default image for the email template.
     */
    protected function setDefaultEmailImage()
    {
        if (!$this->getImage()) {
            $image = $this->getDefaultEmailImage();
            $this->setImageArray($image);
        }
    }

    /**
     * Retrieves default values for the email image.
     *
     * @return array An array representing an image.
     */
    public function getDefaultEmailImage()
    {
        $image = [];
        if (c("Garden.EmailTemplate.Image", "")) {
            $image["source"] = Gdn_UploadImage::url(c("Garden.EmailTemplate.Image"));
        }
        $image["link"] = url("/", true);
        $image["alt"] = c("Garden.LogoTitle", c("Garden.Title", ""));
        return $image;
    }

    /**
     * Filters an unsafe HTML string and returns it.
     *
     * @param string $html The HTML to filter.
     * @param bool $convertNewlines Whether to convert new lines to html br tags.
     * @param bool $filter Whether to escape HTML or not.
     *
     * @return string The filtered HTML string.
     */
    protected function formatContent($html, $convertNewlines = false, $filter = false)
    {
        $str = $html;
        if ($filter) {
            $str = Gdn_Format::htmlFilter($str);
        }
        if ($convertNewlines) {
            $str = preg_replace('/(\015\012)|(\015)|(\012)/', "<br>", $str);
        }
        // $str = strip_tags($str, ['b', 'i', 'p', 'strong', 'em', 'br']);
        return $str;
    }

    /**
     * Set which application view to use.
     *
     * @param string $view The view name.
     * @param string $controllerName The controller name for the view.
     * @param string $applicationFolder The application folder for the view.
     *
     * @return EmailTemplate $this The calling object.
     * @throws Exception
     */
    public function setView($view, $controllerName = "email", $applicationFolder = "dashboard")
    {
        $this->view = LegacyAssetModel::viewLocation($view, $controllerName, $applicationFolder);
        return $this;
    }

    /**
     * Get the current email title.
     *
     * @return string The HTML formatted email title.
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Set the current email title.
     *
     * @param string $title The HTML formatted email title.
     *
     * @return EmailTemplate $this The calling object.
     */
    public function setTitle($title)
    {
        $this->title = $this->formatContent($title);
        return $this;
    }

    /**
     * Get the current email sub-title.
     *
     * @return string The HTML formatted email lead (sub-title, appears under title).
     */
    public function getLead()
    {
        return $this->lead;
    }

    /**
     * Set the current email sub-title.
     *
     * @param string $lead The HTML formatted email lead (sub-title, appears under title).
     *
     * @return EmailTemplate $this The calling object.
     */
    public function setLead($lead)
    {
        $this->lead = $this->formatContent($lead);
        return $this;
    }

    /**
     * Get the main body of the email.
     *
     * @return string The HTML formatted email message (the body of the email).
     */
    public function getMessage()
    {
        return $this->message;
    }

    /**
     * Set the main body of the email.
     *
     * @param string $message The HTML formatted email message (the body of the email).
     * @param bool $convertNewlines Whether to convert new lines to html br tags.
     * @param bool $filter Whether to filter HTML or not.
     *
     * @return EmailTemplate $this The calling object.
     */
    public function setMessage($message, $convertNewlines = false, $filter = false)
    {
        $this->message = $this->formatContent($message, $convertNewlines, $filter);
        return $this;
    }

    /**
     * Get the HTML footer.
     *
     * @return array{text: string, textColor:string, backgroundColor: string} The HTML formatted email footer.
     */
    public function getFooter()
    {
        return $this->footer;
    }

    /**
     * Sets the HTML footer.
     *
     * The footer background and text colors default to the button background and text colors.
     *
     * @param string $html The HTML formatted email footer text.
     * @return EmailTemplate $this The calling object.
     */
    public function setFooterHtml(string $html)
    {
        $textColor = $this->textColor;
        $backgroundColor = $this->backgroundColor;

        // Make sure the links have proper styling.
        $textDoc = new HtmlDocument($html);
        $links = $textDoc->queryCssSelector("a");

        /** @var DOMElement $link */
        foreach ($links as $link) {
            $link->setAttribute("style", "color: {$textColor}; font-weight: 600;");
        }
        $html = $textDoc->getInnerHtml();

        $this->footer = [
            "text" => $html,
            "textColor" => htmlspecialchars($textColor),
            "backgroundColor" => htmlspecialchars($backgroundColor),
        ];
        return $this;
    }

    /**
     * Get the main text color.
     *
     * @return string The hex color code of the text.
     */
    public function getTextColor()
    {
        return $this->textColor;
    }

    /**
     * Set the main text color. Chainable.
     *
     * @param string $color The hex color code of the text, must include the leading '#'.
     *
     * @return EmailTemplate $this The calling object.
     */
    public function setTextColor($color)
    {
        $this->textColor = htmlspecialchars($color);
        return $this;
    }

    /**
     *
     *
     * @return string The hex color code of the background.
     */
    public function getBackgroundColor()
    {
        return $this->backgroundColor;
    }

    /**
     *
     *
     * @param string $color The hex color code of the background, must include the leading '#'.
     *
     * @return EmailTemplate $this The calling object.
     */
    public function setBackgroundColor($color)
    {
        $this->backgroundColor = htmlspecialchars($color);
        return $this;
    }

    /**
     *
     *
     * @return string The hex color code of the container background.
     */
    public function getContainerBackgroundColor()
    {
        return $this->containerBackgroundColor;
    }

    /**
     *
     *
     * @param string $color The hex color code of the container background, must include the leading '#'.
     *
     * @return EmailTemplate $this The calling object.
     */
    public function setContainerBackgroundColor($color)
    {
        $this->containerBackgroundColor = htmlspecialchars($color);
        return $this;
    }

    /**
     *
     *
     * @return string The default hex color code of the button text, must include the leading '#'.
     */
    public function getDefaultButtonTextColor()
    {
        return $this->defaultButtonTextColor;
    }

    /**
     * Sets the default color for the button text.
     *
     * The text color of the EmailTemplate's button property can be overridden by setting $button['textColor']
     *
     * @param string $color The default hex color code of the button text, must include the leading '#'.
     * @return EmailTemplate $this The calling object.
     */
    public function setDefaultButtonTextColor($color)
    {
        $this->defaultButtonTextColor = $color;
        return $this;
    }

    /**
     *
     *
     * @return string The default hex color code of the button background, must include the leading '#'.
     */
    public function getDefaultButtonBackgroundColor()
    {
        return $this->defaultButtonBackgroundColor;
    }

    /**
     * Sets the default color for the button background.
     *
     * The background color of the EmailTemplate's button property can be overridden by setting $button['backgroundColor']
     *
     * @param string $color The default hex color code of the button background, must include the leading '#'.
     * @return EmailTemplate $this The calling object.
     */
    public function setDefaultButtonBackgroundColor($color)
    {
        $this->defaultButtonBackgroundColor = $color;
        return $this;
    }

    /**
     * Get the `plaintext` property.
     *
     * @return bool Whether to render in plaintext.
     */
    public function isPlaintext()
    {
        return $this->plaintext;
    }

    /**
     * Set the `plaintext` property.
     *
     * @param bool $plainText Whether to render in plaintext.
     */
    public function setPlaintext($plainText)
    {
        $this->plaintext = $plainText;
    }

    /**
     * Set the button property.
     *
     * @param string $url The href value of the button.
     * @param string $text The button text.
     * @param string $textColor The hex color code of the button text, must include the leading '#'.
     * @param string $backgroundColor The hex color code of the button background, must include the leading '#'.
     * @return EmailTemplate $this The calling object.
     */
    public function setButton($url, $text, $textColor = "", $backgroundColor = "")
    {
        if (!$textColor) {
            $textColor = $this->defaultButtonTextColor;
        }
        if (!$backgroundColor) {
            $backgroundColor = $this->defaultButtonBackgroundColor;
        }
        $this->button = [
            "url" => htmlspecialchars($url),
            "text" => htmlspecialchars($this->formatContent($text)),
            "textColor" => htmlspecialchars($textColor),
            "backgroundColor" => htmlspecialchars($backgroundColor),
        ];
        return $this;
    }

    /**
     * Get the URL for the button.
     *
     * @return string
     */
    public function getButtonUrl(): string
    {
        return html_entity_decode($this->button["url"] ?? "");
    }

    /**
     * Get the text of the button.
     *
     * @return string
     */
    public function getButtonText(): string
    {
        return html_entity_decode($this->button["text"] ?? "");
    }

    /**
     * Remove the button.
     *
     * @return EmailTemplate $this The calling object.
     */
    public function removeButton()
    {
        $this->button = [];
        return $this;
    }

    /**
     *
     *
     * @return array An array representing an image with the following keys:
     *  'source' => The image source url.
     *  'link' => The href value of the image wrapper.
     *  'alt' => The alt value of the image tag.
     */
    public function getImage()
    {
        return $this->image;
    }

    /**
     * Set the image property.
     *
     * @param string $sourceUrl The image source url.
     * @param string $linkUrl  The href value of the image wrapper.
     * @param string $alt The alt value of the img tag.
     *
     * @return EmailTemplate $this The calling object.
     */
    public function setImage($sourceUrl = "", $linkUrl = "", $alt = "")
    {
        // We need either a source image or an alt to have an img tag.
        if ($sourceUrl || $alt) {
            $this->image = [
                "source" => htmlspecialchars($sourceUrl),
                "link" => htmlspecialchars($linkUrl),
                "alt" => htmlspecialchars($alt),
            ];
        }
        return $this;
    }

    /**
     * Set the image properties.
     *
     * @param array $image Uses the following keys:
     *  'source' => The image source url.
     *  'link' => The href value of the image wrapper.
     *  'alt' => The alt value of the img tag.
     *
     * @return EmailTemplate $this The calling object.
     */
    public function setImageArray($image)
    {
        $this->setImage(val("source", $image), val("link", $image), val("alt", $image));
        return $this;
    }

    /**
     * Copies the email object to an array.
     *
     * A simple (array) typecast won't work since the properties are protected
     * and, as such, add unwanted information to the array keys.
     *
     * @param EmailTemplate $email The email object.
     *
     * @return array Copy of email object in an array format for output.
     */
    protected function objectToArray($email)
    {
        if (is_array($email) || is_object($email)) {
            $result = [];
            foreach ($email as $key => $value) {
                $result[$key] = $this->objectToArray($value);
            }
            return $result;
        }
        return $email;
    }

    /**
     * Renders a plaintext email.
     *
     * @return string A plaintext email.
     */
    protected function plainTextEmail()
    {
        $formatService = Gdn::formatService();

        $email = [
            "banner" => val("alt", $this->image) . " " . val("link", $this->image),
            "title" => $this->getTitle(),
            "lead" => $this->getLead(),
            "message" => $this->getMessage(),
            "button" => sprintf(t("%s: %s"), val("text", $this->button), val("url", $this->button)),
            "footer" => $this->getFooter(),
        ];

        foreach ($email as $key => $val) {
            if (!$val) {
                unset($email[$key]);
            } else {
                // Add some additional padding, where necessary, to improve the appearance of plain-text messages.
                if ($key == "message") {
                    $email[$key] = "<br>$val<br>";
                } elseif ($key == "banner") {
                    $email["banner"] = "{$val}<br>";
                } elseif ($key == "footer") {
                    $email["footer"] = "<br>{$val["text"]}<br/>";
                }
            }
        }

        $body = implode("<br>", $email);
        // Treat the HTML as WYSIWYG for the format's preferred handling of breaks/newlines.
        $result = $formatService->renderPlainText($body, WysiwygFormat::FORMAT_KEY);
        return $result;
    }

    /**
     * Render the email.
     *
     * @return string The rendered email.
     */
    public function toString()
    {
        if ($this->isPlaintext()) {
            return $this->plainTextEmail();
        }
        $controller = new Gdn_Controller();
        $controller->setData("email", $this->objectToArray($this));
        $email = $controller->fetchView($this->view);

        // Append plaintext version
        $email .= self::PLAINTEXT_START . $this->plainTextEmail();
        return $email;
    }
}
